package org.dicr.sys.linux;

import java.io.*;
import java.lang.ref.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;

import org.dicr.sys.*;
import org.dicr.util.data.*;
import org.dicr.util.data.exc.*;
import org.dicr.util.net.*;

/**
 * Файл DHCP хостов.
 * <P>
 * Класс служит для работы с файлом DHCP хостов.
 * </P>
 * <P>
 * Хосты идентифицируются по имени. Имя хоста должно быть уникальным, а
 * одинаковый ip и(или) mac-адрес могут разделять несколько хостов.
 * </P>
 * <P>
 * Файл должен иметь формат:
 * 
 * <PRE>
 * 
 * host &lt;I&gt;host1&lt;/I&gt; { hardware ethernet &lt;I&gt;00:01:02:03:04:05&lt;/I&gt;; fixed-address &lt;I&gt;192.168.0.2&lt;/I&gt;; } ... ... host
 * &lt;I&gt;host2&lt;/I&gt; { hardware ethernet &lt;I&gt;02:03:04:05:06:07&lt;/I&gt;; fixed-address &lt;I&gt;192.168.0.3&lt;/I&gt;; }
 * 
 * </PRE>
 * 
 * </P>
 * <P>
 * Для того чтобы вставить файл в конфигурацию dhcpd.conf можно использовать
 * директиву <CODE>include</CODE>, например:
 * 
 * <PRE>
 * 
 * subnet &lt;I&gt;192.168.48.0&lt;/I&gt; netmask &lt;I&gt;255.255.255.0&lt;/I&gt; { option routers 192.168.48.1; include
 * &quot;/etc/dhcpd.192.168.48.conf&quot;; }
 * 
 * </PRE>
 * 
 * </P>
 * <P>
 * Для управления доступом к физическому файлу разных потоков создается реестр
 * создагнных обьектов. Если обьект, связанный с данным файлом уже существует,
 * то возвращается уже существующий экземпляр, иначе создается новый.
 * </P>
 * 
 * @author Igor A Tarasov, &lt;java@dicr.org&gt;
 * @version 1.0
 * @todo full suport of /etc/dhcpd.conf with includes.
 */
public class DHCPHostsFile extends SystemFileHandler {

	private static final Logger log = Logger.getLogger(DHCPHostsFile.class.getName());

	private static final Pattern hostLinePattern = Pattern.compile("\\s*host\\s+(\\S+)\\s*\\{\\s*");

	private static final Pattern macLinePattern = Pattern
			.compile("\\s*hardware\\s+ethernet\\s+([0-9a-fA-F\\:]+)\\s*\\;\\s*");

	private static final Pattern ipLinePattern = Pattern.compile("\\s*fixed\\-address\\s+([0-9\\.]+)\\s*\\;\\s*");

	/** Имя файла хостов по умолчанию. */
	public static final String DEFAULT_FILE = "/etc/dhcpd.hosts.conf";

	private long fileTime = 0;

	private SoftReference<Map<String, DHCPHost>> hostsRef = null;

	private DHCPHostsFile(String fileName) throws FileLockedException {
		super(fileName);
		this.fileTime = 0;
		this.hostsRef = null;
	}

	/**
	 * Return DHCP Hosts file handler
	 * 
	 * @param fileName
	 *            file name
	 * @return instance of object
	 * @throws FileLockedException
	 *             file already locked
	 */
	public static synchronized DHCPHostsFile getInstance(String fileName) throws FileLockedException {
		if (fileName == null) {
			throw new IllegalArgumentException("null file name");
		}
		String absName = new File(fileName).getAbsolutePath();
		DHCPHostsFile dhcpFileHandler = null;
		SystemFileHandler handler = getHandler(absName);
		if (handler != null) {
			if (handler instanceof DHCPHostsFile) dhcpFileHandler = (DHCPHostsFile) handler;
			else throw new FileLockedException(absName);
		} else {
			dhcpFileHandler = new DHCPHostsFile(absName);
		}
		return dhcpFileHandler;
	}

	/**
	 * Return default file handler.
	 * 
	 * @return singleton instance
	 * @throws FileLockedException
	 */
	public static DHCPHostsFile getInstance() throws FileLockedException {
		return getInstance(DEFAULT_FILE);
	}

	/**
	 * Read file content
	 * 
	 * @return parsed data
	 * @throws IOException
	 *             error reading file
	 */
	private synchronized Map<String, DHCPHost> getFileData() throws IOException {
		// проверяем возможность кэширования
		Map<String, DHCPHost> hosts = null;
		if (this.hostsRef != null) hosts = this.hostsRef.get();
		if (hosts != null && this.fileTime != 0 && new File(getFileName()).lastModified() <= this.fileTime) return hosts;
		// сбрасываем кэш
		this.hostsRef = null;
		this.fileTime = 0;
		hosts = new HashMap<>();
		// парсим файл
		BufferedReader in = null;
		try {
			in = new BufferedReader(new FileReader(getFileName()));
			String name = null;
			IP ip = null;
			MAC mac = null;
			for (int lineNo = 1;; lineNo++) {
				try {
					String line = in.readLine();
					if (line == null) break;
					line = StringUtils.normalize(line);
					if (line == null || line.startsWith("//") || line.startsWith("#")) continue;
					line = line.toLowerCase();
					Matcher hostMatcher = hostLinePattern.matcher(line);
					Matcher ipMatcher = ipLinePattern.matcher(line);
					Matcher macMatcher = macLinePattern.matcher(line);
					if (hostMatcher.matches()) {
						if (name != null) throw new FileFormatException(getFileName(), lineNo);
						name = hostMatcher.group(1);
					} else if (macMatcher.matches()) mac = new MAC(macMatcher.group(1));
					else if (ipMatcher.matches()) ip = new IP(ipMatcher.group(1));
					else if (line.startsWith("}")) {
						if (name == null) throw new FileFormatException(getFileName(), lineNo);
						if (hosts.containsKey(name)) log.warning(String.format("дублирование хоста name=" + name
								+ " в файле " + getFileName()));
						hosts.put(name, new DHCPHost(name, mac, ip));
						name = null;
					}
				} catch (FileFormatException ex) {
					log.log(Level.WARNING, "ошибка в файле", ex);
				} catch (IncorrectAddressException ex) {
					log.log(Level.WARNING, "ошибка адреса", ex);
				}
			}
			// сохраняем кэш
			this.fileTime = new File(getFileName()).lastModified();
			this.hostsRef = new SoftReference<>(hosts);
		} catch (FileNotFoundException ex) {
			log.log(Level.WARNING, "файл " + getFileName() + " не существует", ex);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Exception ex) {
					// NOP
				}
			}
		}
		return hosts;
	}

	/**
	 * Сохраняет данные в файл.
	 * <P>
	 * Сохраняемые данные также помещаются в кэш.
	 * </P>
	 * 
	 * @param hosts
	 *            данные для записи.
	 * @throws IOException
	 *             ошибка записи файла.
	 */
	protected synchronized void saveFileData(Map<String, DHCPHost> hosts) throws IOException {
		assert (hosts != null);
		PrintWriter ou = null;
		try {
			ou = new PrintWriter(new FileWriter(getFileName()));
			Collection<DHCPHost> values = hosts.values();
			for (DHCPHost host : values) {
				ou.printf("host %s {\n", host.getName());
				ou.printf("\thardware ethernet\t%s;\n", host.getMac());
				ou.printf("\tfixed-address\t\t%s;\n", host.getIp());
				ou.println("}");
			}
			// update cache
			this.hostsRef = new SoftReference<>(hosts);
			this.fileTime = new File(getFileName()).lastModified();
		} finally {
			try {
				if (ou != null) ou.close();
			} catch (Exception ex) {
				// NOP
			}
		}
	}

	/**
	 * Возвращает список хостов.
	 * 
	 * @return список хостов файла.
	 * @throws IOException
	 *             ошибка чтения файла.
	 */
	public synchronized DHCPHost[] list() throws IOException {
		return getFileData().values().toArray(new DHCPHost[0]);
	}

	/**
	 * Return host by name
	 * 
	 * @param name
	 *            name of configured host
	 * @return instance of host object
	 * @throws NotFoundException
	 *             host not found
	 * @throws IOException
	 *             file reading exception
	 */
	public synchronized DHCPHost findByName(String name) throws NotFoundException, IOException {
		if (name == null) throw new IllegalArgumentException("null name");
		DHCPHost host = getFileData().get(name);
		if (host == null) throw new NotFoundException("host name", name);
		return host;
	}

	/**
	 * Return host by MAC-address
	 * 
	 * @param mac
	 *            MAC-address to search
	 * @return host object
	 * @throws NotFoundException
	 *             host not found
	 * @throws IOException
	 *             error reading file data
	 */
	public synchronized DHCPHost findByMac(MAC mac) throws NotFoundException, IOException {
		if (mac == null) throw new IllegalArgumentException("null mac");
		DHCPHost result = null;
		Collection<DHCPHost> hosts = getFileData().values();
		for (DHCPHost host : hosts) {
			if (host.getMac().equals(mac)) {
				result = host;
				break;
			}
		}
		if (result == null) throw new NotFoundException("host mac", mac);
		return result;
	}

	/**
	 * Find host by IP-address
	 * 
	 * @param ip
	 *            ip-address to search
	 * @return host object
	 * @throws NotFoundException
	 *             host not found
	 * @throws IOException
	 *             error reading data
	 */
	public synchronized DHCPHost findByIp(IP ip) throws NotFoundException, IOException {
		if (ip == null) {
			throw new IllegalArgumentException("null ip");
		}
		DHCPHost result = null;
		Collection<DHCPHost> hosts = getFileData().values();
		for (DHCPHost host : hosts) {
			if (host.getIp().equals(ip)) {
				result = host;
				break;
			}
		}
		if (result == null) {
			throw new NotFoundException("host ip", ip);
		}
		return result;
	}

	/**
	 * Create new host
	 * 
	 * @param name
	 *            host name
	 * @param mac
	 *            MAC-address
	 * @param ip
	 *            IP-address
	 * @throws DuplicateException
	 *             host already exists
	 * @throws IOException
	 *             error reading/whiting file data
	 */
	public synchronized void create(String name, MAC mac, IP ip) throws DuplicateException, IOException {
		String _name = StringUtils.normalize(name);
		if (_name == null) throw new IllegalArgumentException("null name");
		if (mac == null) throw new IllegalArgumentException("null mac");
		if (ip == null) throw new IllegalArgumentException("null ip");
		Map<String, DHCPHost> data = getFileData();
		if (data.containsKey(_name)) throw new DuplicateException("host name", _name);
		data.put(_name, new DHCPHost(_name, mac, ip));
		saveFileData(data);
		log.fine("создан хост name=" + _name + ", mac=" + mac + ", ip=" + ip);
	}

	/**
	 * Rename host
	 * 
	 * @param oldname
	 *            old name
	 * @param newname
	 *            new name
	 * @throws NotFoundException
	 *             host not found
	 * @throws DuplicateException
	 *             host already exists
	 * @throws IOException
	 *             I/O error
	 */
	public synchronized void rename(String oldname, String newname) throws NotFoundException, DuplicateException,
			IOException {
		if (oldname == null) throw new IllegalArgumentException("null old name");
		if (newname == null) throw new IllegalArgumentException("empty new name");
		if (newname.equals(oldname)) return;
		Map<String, DHCPHost> data = getFileData();
		DHCPHost host = data.remove(oldname);
		if (host == null) throw new NotFoundException("host name", oldname);
		if (data.containsKey(newname)) throw new DuplicateException("host name", newname);
		data.put(newname, new DHCPHost(newname, host.getMac(), host.getIp()));
		saveFileData(data);
		log.fine("переименов хост " + oldname + " в " + newname);
	}

	/**
	 * Set host MAC
	 * 
	 * @param name
	 *            name of host
	 * @param mac
	 *            new MAC-address
	 * @throws NotFoundException
	 *             host not found
	 * @throws IOException
	 *             error reading/writing file data
	 */
	public synchronized void setMac(String name, MAC mac) throws NotFoundException, IOException {
		if (name == null) throw new IllegalArgumentException("null name");
		if (mac == null) throw new IllegalArgumentException("null mac");
		Map<String, DHCPHost> data = getFileData();
		DHCPHost host = data.remove(name);
		if (host == null) throw new NotFoundException("host name", name);
		data.put(name, new DHCPHost(name, mac, host.getIp()));
		saveFileData(data);
		log.fine("изменен mac-адрес хоста " + name + " на " + mac);
	}

	/**
	 * Set host IP
	 * 
	 * @param name
	 *            hst name
	 * @param ip
	 *            IP-address
	 * @throws NotFoundException
	 *             host not found
	 * @throws IOException
	 *             I/O error
	 */
	public synchronized void setIp(String name, IP ip) throws NotFoundException, IOException {
		if (name == null) throw new IllegalArgumentException("null name");
		if (ip == null) throw new IllegalArgumentException("null ip");
		Map<String, DHCPHost> data = getFileData();
		DHCPHost host = data.remove(name);
		if (host == null) throw new NotFoundException("host name", name);
		data.put(name, new DHCPHost(name, host.getMac(), ip));
		saveFileData(data);
		log.fine("изменен ip-адрес хоста " + name + " на " + ip);
	}

	/**
	 * Remove host
	 * 
	 * @param name
	 *            host name
	 * @throws NotFoundException
	 *             host not found
	 * @throws IOException
	 *             I/O error
	 */
	public synchronized void remove(String name) throws NotFoundException, IOException {
		if (name == null) throw new IllegalArgumentException("null name");
		Map<String, DHCPHost> data = getFileData();
		if (data.remove(name) == null) throw new NotFoundException("host name", name);
		saveFileData(data);
		log.fine("удален хост " + name);
	}

}
